package sm.util;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * a data map that can handle field path, e.g.: put("a.b.c", 1) and put("a[0]")
 * 
 * @author dingha
 * 
 */
public class SMMap implements Map<String, Object>, Cloneable, Serializable {

	private static final long serialVersionUID = -504784057820151017L;

	private Map<String, Object> datas = new HashMap<String, Object>();

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((datas == null) ? 0 : datas.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		SMMap other = (SMMap) obj;
		if (datas.size() != other.datas.size()) {
			return false;
		} else {
			for (Entry<String, Object> entry : datas.entrySet()) {
				String key = entry.getKey();
				Object value = entry.getValue();
				if (!Util.equals(value, other.datas.get(key))) {
					return false;
				}
			}
		}
		return true;
	}

	public Object clone() {
		try {
			SMMap v = (SMMap) super.clone();
			v.datas = new HashMap<String, Object>();
			for (Entry<String, Object> data : datas.entrySet()) {
				String key = data.getKey();
				Object value = data.getValue();
				v.datas.put(key, Util.cloneData(value));
			}
			return v;
		} catch (CloneNotSupportedException e) {
			// this shouldn't happen, since we are Cloneable
			throw new InternalError();
		}
	}

	@Override
	public int size() {
		return datas.size();
	}

	@Override
	public boolean isEmpty() {
		return datas.isEmpty();
	}

	@Override
	public boolean containsValue(Object value) {
		return datas.containsValue(value);
	}

	@Override
	public void putAll(Map<? extends String, ? extends Object> m) {
		datas.putAll(m);
	}

	@Override
	public void clear() {
		datas.clear();
	}

	@Override
	public Set<String> keySet() {
		return datas.keySet();
	}

	@Override
	public Collection<Object> values() {
		return datas.values();
	}

	@Override
	public Set<java.util.Map.Entry<String, Object>> entrySet() {
		return datas.entrySet();
	}

	private boolean containsKey(Map<String, Object> values, String[] fields) {
		boolean containsField = false;
		int depth = fields.length;
		for (int i = 0; i < depth; i++) {
			String field = fields[i];
			if (i < depth - 1) {
				Object value = values.get(field);
				if (value instanceof Map) {
					values = (Map<String, Object>) value;
				} else {
					// wrong path
					break;
				}
			} else {
				containsField = values.containsKey(field);
			}
		}
		return containsField;
	}

	@Override
	public boolean containsKey(Object key) {
		if (key instanceof String) {
			String keyStr = (String) key;
			int pathIdx = keyStr.indexOf(".");
			if (pathIdx != -1) {
				return containsKey(datas, keyStr.split("\\."));
			}
		}

		return datas.containsKey(key);
	}

	private Object removeKey(Map<String, Object> values, String[] fields) {
		Object oldValue = null;
		int depth = fields.length;
		for (int i = 0; i < depth; i++) {
			String field = fields[i];
			if (i < depth - 1) {
				Object value = values.get(field);
				if (value instanceof Map) {
					values = (Map<String, Object>) value;
				} else {
					// wrong path
					break;
				}
			} else {
				oldValue = values.remove(field);
			}
		}
		return oldValue;
	}

	@Override
	public Object remove(Object key) {
		if (key instanceof String) {
			String keyStr = (String) key;
			int pathIdx = keyStr.indexOf(".");
			if (pathIdx != -1) {
				return removeKey(datas, keyStr.split("\\."));
			}
		}

		return datas.remove(key);
	}

	private Object get(Map<String, Object> values, String[] fields) {
		Object value = null;
		int depth = fields.length;
		for (int i = 0; i < depth; i++) {
			String field = fields[i];
			if (i < depth - 1) {
				Object fieldValue = values.get(field);
				if (fieldValue instanceof Map) {
					values = (Map<String, Object>) fieldValue;
				} else {
					// wrong path
					value = null;
					break;
				}
			} else {
				value = values.get(field);
			}
		}
		return value;
	}

	@Override
	public Object get(Object key) {
		if (key instanceof String) {
			String keyStr = (String) key;
			int pathIdx = keyStr.indexOf(".");
			if (pathIdx != -1) {
				return get(datas, keyStr.split("\\."));
			}
		}

		return datas.get(key);
	}

	private Object put(Map<String, Object> values, String[] fields, Object value) {
		Object oldValue = null;
		int depth = fields.length;
		for (int i = 0; i < depth; i++) {
			String field = fields[i];
			if (i < depth - 1) {
				Object fieldValue = values.get(field);
				if (fieldValue == null) {
					fieldValue = new SMMap();
					values.put(field, fieldValue);
				}

				if (fieldValue instanceof Map) {
					values = (Map<String, Object>) fieldValue;
				} else {
					// wrong path
					break;
				}
			} else {
				oldValue = putValue(values, field, value);
			}
		}
		return oldValue;
	}

	/**
	 * do not create new record for exist map (merge it), unless remove it first
	 * 
	 * @param values
	 * @param key
	 * @param value
	 * @return
	 */
	private Object putValue(Map<String, Object> values, String key, Object value) {
		Object oldValue = null;
		boolean createNewRecord = true;
		if (value instanceof Map) {
			oldValue = values.get(key);
			if (oldValue instanceof Map) {
				Map<String, Object> valueMap = (Map<String, Object>) value;
				Map<String, Object> oldValueMap = (Map<String, Object>) oldValue;
				// do not create new record for exist map
				// unless remove it first
				createNewRecord = false;
				oldValueMap.putAll(valueMap);
			}
		}

		if (createNewRecord) {
			oldValue = values.put(key, value);
		}
		return oldValue;
	}

	@Override
	public Object put(String key, Object value) {
		Object oldValue = null;
		int pathIdx = key.indexOf(".");
		if (pathIdx != -1) {
			oldValue = put(datas, key.split("\\."), value);
		} else {
			oldValue = putValue(datas, key, value);
		}

		return oldValue;
	}

}
